Climate Suitability Widget

Climate Suitability Widget#

Hide code cell source
# Imports
import pandas as pd
import altair as alt
alt.data_transformers.disable_max_rows()  # allows all rows
import ipywidgets as widgets
from IPython.display import display, clear_output
import os


#alt.data_transformers.enable('vegafusion');
#alt.data_transformers.disable_max_rows();
# Load and preprocess CAT data with dynamic path
data_path = 'data/CAT-current-UBCBG.csv'
if not os.path.exists(data_path) and os.path.exists('../data/CAT-current-UBCBG.csv'):
    data_path = '../data/CAT-current-UBCBG.csv'

cat_df = pd.read_csv(data_path)

# Rename climate rating columns
cat_df = cat_df.rename(columns={
    'TaxonName': 'Taxon',
    'ProvenanceCode': 'ProvenanceGroup',
    'ClimateRating_current': 'Current',
    'ClimateRating_emissions-limited_2050': '2050',
    'ClimateRating_business-as-usual_2090': '2090',
    'ClimateRating_bau-plus-1-degree_2090': '2090_plus_1deg'
})

# Fold into long format
long_df = cat_df.melt(
    id_vars=['ItemAccNoFull', 'ItemLocationCode', 'LocationCoordX', 'LocationCoordY', 
             'Taxon', 'LifeForm', 'ProvenanceGroup', 'LocationName'],
    value_vars=['Current', '2050', '2090', '2090_plus_1deg'],
    var_name='Era',
    value_name='ClimateRating'
)

# Filter to Main Garden
garden_df = long_df[
    (~long_df['LocationName'].str.contains('Nursery', na=False)) &
    (~long_df['LocationName'].str.contains('Nitobe', na=False))
].dropna(subset=['LocationCoordX', 'LocationCoordY'])
# Map LifeForm into simplified groups
def map_lifeform(lifeform):
    if pd.isna(lifeform):
        return 'Unknown'
    if lifeform in ['Tree', 'Shrub or Tree']:
        return 'Trees'
    if lifeform == 'Herbaceous Perennial':
        return 'Perennials'
    if lifeform == 'Annual':
        return 'Annuals'
    if lifeform == 'Bulb, Corm, or Tuber':
        return 'Bulbs'
    if lifeform in ['Shrub', 'Climber_Liana_Vine']:
        return 'Woody'
    if lifeform in ['Annual', 'Herbaceous Perennial', 'Bulb, Corm, or Tuber']:
        return 'Herbaceous'
    return 'Other'

garden_df['LifeFormGroup'] = garden_df['LifeForm'].apply(map_lifeform)
Hide code cell source
# Set up dropdown widgets
era_widget = widgets.Dropdown(
    options=['Current', '2050', '2090', '2090_plus_1deg'],
    value='Current',
    description='Era:'
)

lifeform_widget = widgets.Dropdown(
    options=['(All)', 'Trees', 'Perennials', 'Annuals', 'Bulbs', 'Woody', 'Herbaceous', 'Other'],
    value='(All)',
    description='LifeForm:'
)

provenance_widget = widgets.Dropdown(
    options=['(All)'] + sorted(garden_df['ProvenanceGroup'].dropna().unique()),
    value='(All)',
    description='Provenance:'
)

output = widgets.Output()
# Define fixed Garden boundaries
manual_long_min = -123.2525
manual_long_max = -123.2402
manual_lat_min = 49.248
manual_lat_max = 49.256
# Plot update function
def update_plot(*args):
    filtered = garden_df[garden_df['Era'] == era_widget.value]
    
    if lifeform_widget.value != '(All)':
        filtered = filtered[filtered['LifeFormGroup'] == lifeform_widget.value]
        
    if provenance_widget.value != '(All)':
        filtered = filtered[filtered['ProvenanceGroup'] == provenance_widget.value]
    
    filtered = filtered.dropna(subset=['ClimateRating'])
    
    chart = alt.Chart(filtered).mark_circle(size=60).encode(
        x=alt.X('LocationCoordY:Q', scale=alt.Scale(domain=[manual_long_min, manual_long_max]), axis=alt.Axis(title='Longitude')),
        y=alt.Y('LocationCoordX:Q', scale=alt.Scale(domain=[manual_lat_min, manual_lat_max]), axis=alt.Axis(title='Latitude')),
        color=alt.Color('ClimateRating:Q', scale=alt.Scale(domain=[5, 11], range=['red', 'yellow', 'green'])),
        tooltip=['Taxon:N', 'ClimateRating:Q', 'LocationCoordX:Q', 'LocationCoordY:Q']
    ).properties(
        width=600,
        height=600,
        title=f"Climate Rating Map ({era_widget.value})"
    )
    
    with output:
        clear_output(wait=True)
        if not filtered.empty:
            display(chart)
        else:
            display(alt.Chart(pd.DataFrame({'x':[], 'y':[]})).mark_point())
Hide code cell source
# Apply layout fixes
for w in [era_widget, lifeform_widget, provenance_widget]:
    w.layout = widgets.Layout(width='200px')

output.layout = widgets.Layout(width='650px')  # adjust to match chart size

# Rebuild UI layout
ui = widgets.VBox([
    widgets.HBox([
        widgets.VBox([era_widget, lifeform_widget, provenance_widget]),
        output
    ])
])

# Attach observers to update the chart on user interaction
for w in [era_widget, lifeform_widget, provenance_widget]:
    w.observe(update_plot, names='value')

# Display the interface
display(ui)

# Trigger the initial plot once at load
update_plot()